Skip to content

Fix #10129: Fix comparisons of applied type aliases #10221

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Nov 14, 2020

Conversation

odersky
Copy link
Contributor

@odersky odersky commented Nov 7, 2020

Given

type F[Xs] = ...
F[As] <: F[Bs]

Do we dealias F or check the arguments directly? It turns out that neither choice
is correct in all instances. Checking the arguments directly is necessary for hk
type inference, but may narrow the constraint too much. The solution is to use
an either that tries both alternatives.

@odersky odersky marked this pull request as draft November 7, 2020 13:22
@odersky odersky changed the title Fix #10129: Fix comparisons of applied types Fix #10129: Fix comparisons of applied type aliases Nov 7, 2020
@odersky odersky closed this Nov 7, 2020
@odersky odersky reopened this Nov 7, 2020
@odersky odersky marked this pull request as ready for review November 7, 2020 14:54
Comment on lines +1008 to +1010
// This leads to the constraint Lifted[U] <: Lifted[Int]. If we just
// check the arguments this gives `U <: Int`. But this is wrong. Dealiasing
// `Lifted` gives `Err | U <: Err | Int`, hence it should be `U <: Err | Int`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not necessarily against changing it now if we believe it's worth it, but that was actually an intentional behavior, see the justification in 5fb13aa:

I think the new behavior is arguably better since using type aliases now
gives you more control on how type inference proceeds, even if it means
that some things that used to typecheck don't anymore.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should not simply infer something that does not follow from the constraint. Or at least we should do it under the strict control of either. i10129 is arguably a very natural way to express things.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or, let's put it that way. The correct thing to do is to dealias. Unfortunately that breaks hk type inference. But hk type inference always was an ad-hoc best effort thing (some might call it a kludge). It's clear we can't do hk type inference in general. So it's good to push this as far as we can, but it should not compromise the correct behavior for other constraints.

@odersky
Copy link
Contributor Author

odersky commented Nov 7, 2020

test performance please

@dottybot
Copy link
Member

dottybot commented Nov 7, 2020

performance test scheduled: 1 job(s) in queue, 0 running.

@dottybot
Copy link
Member

dottybot commented Nov 7, 2020

Performance test finished successfully:

Visit http://dotty-bench.epfl.ch/10221/ to see the changes.

Benchmarks is based on merging with master (5c84e42)

// that is taken care of by the outer inFrozenGadtIf test. The problem is
// that hk-alias-unification.scala relies on the right isSubArgs being performed
// when constraining the result type of a method. But there we are only allowed
// to use a necessaryEither, since it might be that an implicit conversion will
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no implicit conversion in hk-alias-unification.scala, just implicit search.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right. The change was actually introduced here: #8635

It was a way to avoid overcommitting when constraining the result type. Unfortunately, it seems that this causes the problems we encounter here. So, what to do? All this is getting more and more ad-hoc, which is not very satisfactory.

Comment on lines 1078 to 1079
// be inserted on the result. So strictly speaking by going to a sufficientEither
// we overshoot and narrow the constraint unnecessarily. On the other hand, this
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In situation where the gadt constraints aren't frozen, isn't this narrowing problematic? /cc @abgruszecki

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe not, since we are in a frozen Gadt state anyway.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, we're in inFrozenGadtIf(!tyconIsInjective), and tyconIsInjective won't be true if both constructors are type aliases, so gadt constraints are frozen indeed, although it's not immediately obvious from the code.

// which demands a sufficientEither anyway.
sufficientEither(
isSubArgs(args1, args2, tp1, tparams),
recur(tp1.superType, tp2.superType))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We check above that tycon1sym.isAliasType so tp1.superType should be equivalent to tp1, on the other hand I don't see a similar check for tp2, so it seems that tp2.superType could in fact be an actual supertype of tp2.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code is only hit if the two type constructors are the same.

case tp1a as AppliedType(_, args1a) if args1a.eqElements(args1) =>
tp2.superType match
case tp2a as AppliedType(_, args2a) if args2a.eqElements(args2) =>
return recur(tp1a, tp2a)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This branch is never taken (I added an assert here and verified that our whole CI passes with it), because we eagerly dealias in appliedTo unless it affects type inference: https://github.com/lampepfl/dotty/blob/8e9ac7519b56a452348b878979f56ff01308cc8e/compiler/src/dotty/tools/dotc/core/TypeApplications.scala#L319-L325

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, good to know!

Comment on lines 1078 to 1079
// There's one test in shapeless/deriving that had to be disabled
// (modules/deriving/src/test/scala/shapeless3/deriving/deriving.scala, value v7 in the functor test).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI @milessabin, how important is that particular test?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's an important test. It demonstrates the ability to derive functor instances for nested type constructors.

It would at least be good to know why v7 fails whereas v6 succeeds. Both involve recursion. The only difference is that the former failing one has an additional non-recursive wrapper, which is itself successfully derived in v3.

@odersky
Copy link
Contributor Author

odersky commented Nov 10, 2020

That's the second time I get a conflict because of shapeless which causes me a huge effort to re-sync. I'll do that one last time and then merge since I don't have the time to go through these steps a third time.

@milessabin
Copy link
Contributor

The failure with shapeless here is demonstrating what this change is forcing us to give up.

That being the case, it would help to have a motivation for the change and a sketch of a workaround.

@odersky
Copy link
Contributor Author

odersky commented Nov 10, 2020

The motivation is issue #10129.

Here we have

type Lifted[A] = Err | A

and the constraint

Lifted[X] <: Lifted[Int]

It's simply incorrect to conclude from this that X <: Int. So that means in general we have to dealias before comparing. But doing this breaks some higher-kinded type inference moves. So the compromise in this PR is to look at both the direct arguments and the dealiased type and pick the solution that constrains less. That still allows hk type inference to work, unless the dealiased
comparison would constrain things strictly less.

I don't know what else one could do here.

@milessabin
Copy link
Contributor

milessabin commented Nov 10, 2020

That sounds completely reasonable.

What's not clear to me is why that breaks the test in shapeless. Other than involving hk types it doesn't appear to be related in any obvious way to the example you're fixing here.

You suggest this is an inference problem ... is there an explicit type annotation or parameter which could be added to the test which would fix it? What is the compilation error in shapeless for that test?

@odersky
Copy link
Contributor Author

odersky commented Nov 10, 2020

What's not clear to me is why that breaks the test in shapeless. Other than involving hk types it doesn't appear to be related in any obvious way to the example you're fixing here.

It's hard to diagnose since it is in auto-generated code from deriving, I believe. What must have happened is that there is a type alias somewhere such as

    type T[X] = Any

and a constraint gets generated such as

    T[F[A]]  <: T[List{X]]

In this case we'd conclude so far that F = List. But after dealiasing it's just Any <: Any which does not constrain at all. So we do not instantiate F.

The RHS of Any is just for illustration, it could be anything that is already a supertype of the lhs T[F[A]].

@milessabin
Copy link
Contributor

There are uses of a Const type constructor in shapeles, ie.

type Const[c] = [t] =>> c

which could produce something similar to your type T[X] = Any (with something other than Any on the RHS).

Are you saying that this sort of construct can't be relied on any more? Can you suggest an alternative encoding?

@odersky
Copy link
Contributor Author

odersky commented Nov 10, 2020

Yes, Const would be problematic for the same reasons as my example.Not sure what to do. I imagine Const can't be opaque. right?

@smarter
Copy link
Member

smarter commented Nov 10, 2020

I think this comes down to a design decision: do we want type aliases to be a way to drive type inference, or do we want to always infer the minimum set of constraints? The former let library authors craft aliases to improve inference in their APIs and hopefully provide a better UX, but it's ill-defined, whereas the latter can be unintuitive for users but it's a clear criterion compiler developers can refer to when making changes to type inference, so it reduces the risk that we end up piling up heuristics after heuristics without really knowing what we're doing.
I don't know what the correct answer is here, but given that using type aliases to drive inference is already well-established with HK unification which works fairly well in practice, I don't really mind that Lifted[?U] <: Lifted[A] infers ?U <: A (or something else depending on variance) no matter what Lifted is.

@milessabin
Copy link
Contributor

I imagine Const can't be opaque. right?

No, it can't.

@smarter
Copy link
Member

smarter commented Nov 10, 2020

A trick might be to write type Const[c] >: [t] =>> c <: [t] =>> c so the compiler isn't so eager to get rid of the alias (I think we wouldn't end up triggering the code added in this PR because isAliasType will return false), but even if it works it's not a very nice solution.

@odersky
Copy link
Contributor Author

odersky commented Nov 10, 2020

Const[c] >: [t] =>> c <: [t] =>> c

Yes, that looks promising.

@odersky
Copy link
Contributor Author

odersky commented Nov 12, 2020

How should we proceed? We don't seem to have an alternative to the fix in this PR. I propose we get it in and then see what needs to be done to bring back the failing test in shapeless.

@milessabin
Copy link
Contributor

I'm still unclear why this test fails but others don't.

Const is used pervasively in shapeless, and there's a presumption in the Dotty type class derivation infrastructure that something like Const be workable. If this is an edge case with a workaround it's not a big deal, but then it would help to understand why this is an edge case whereas other uses of Const aren't. If it isn't then there's a chance that this will damage type class derivation across the board.

 - streamline comparisons of applied types
 - optimize sufficientEither to not try the second alternative
   if the first succeeds without narrowing the constraint.
Given

    type F[Xs] = ...
    F[As] <: F[Bs]

Do we dealias `F` or check the arguments directly? It turns out that neither choice
is correct in all instances. Checking the arguments directly is necessary for hk
type inference, but may narrow the constraint too much. The solution is to go to
an either that tries both options.
@odersky
Copy link
Contributor Author

odersky commented Nov 12, 2020

Good news! I found a way to do what we want and still keep all shapeless tests.

Digging deeper I found that the issue was not what I initially suspected. I suspected that a constraint generated by a isSubArgs was rolled back by the test of the dealiased types. But what happened instead was that we had a failing isSubArgs, and the alternative that recurred on the dealiased types somehow missed something which then led to this error:

-- Error: deriving.scala:104:43 ------------------------------------------------
104 |    val v7 = Functor[[t] =>> CList[Opt[t]]]
    |                                           ^
    |ambiguous implicit arguments: both method functorGen in object Functor and method given_Functor_F in object Functor match type shapeless3.deriving.Functor[
    |  [t] =>> shapeless3.deriving.adts.CList[shapeless3.deriving.adts.Opt[t]]
    |] of parameter ff of method apply in object Functor
1 error found

If we simply fail isMatchingApply on a failed isSubArgs then the test compiles. So this means that the different paths we took for checking the dealiased types somehow made a difference. In the failing example we dealised both types in lockstep whereas in the original code we dealiased first the supertype and then went through a second check via compareApplied1. Presumably that second check could have done some hk inference that we missed by dealiasing both types at the same time.

The fix is simple: Fail if isSubArgs fails. If it succeeds, also check the dealiased types and pick the weaker constraint.

@milessabin
Copy link
Contributor

Good news! I found a way to do what we want and still keep all shapeless tests.

Awesome ... thanks for digging in to this! 😄

@odersky odersky merged commit 93f24a0 into scala:master Nov 14, 2020
@odersky odersky deleted the fix-#10129 branch November 14, 2020 18:06
@Kordyjan Kordyjan added this to the 3.0.0 milestone Aug 2, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants